/**
 *
 *
 *
 *
 */
$.widget("ui.HierarchyBrowser", {


	// default options
	options: {
		// The base URL where the LMF server is deployed, ends with slash
		serverBaseURL: "http://localhost:8080/KiWi2/",
		
		buttonLabel : "Add",
		
		// Callback for the button click event
		buttonClick : null,

		// Callback for case when a suggestion is clicked
		suggestionClicked : null

	},

	_create: function() {
		// Setup the uI
		this._createUI();

		// Setup the model and start loading the taxonomies
		this._model = HierarchyModel();
		this._loadTaxonomies();
	},

	/**
	 * Logs to browser console if it exists. Otherwise is silent.
	 *
	 */
	_log : function(s){
		try {
			if (typeof console != "undefined") {
				console.log(s);
			}
		} catch (e) {
			// Just ignore, nothing to do about this
		}
	},

	destroy: function() {
		$.Widget.prototype.destroy.apply(this, arguments); // default destroy
		// now do other stuff particular to this widget
	},


	///////////////////////////
	// Private Implementation - User Interface - Rendering
	///////////////////////////

	_createUI : function(){
		
		// Create the skeleton UI
		this.element.html(
			"<input type='text'><input type='button' value='" + this.options.buttonLabel + "'>"
			+ "<div class='suggestionsContainer'></div>"
		);

		// Keep references to the elements
		this._txtField = this.element.find("input[type='text']");
		this._suggestionsContainer = this.element.find("div.suggestionsContainer");

		// Bind event handlers: text field and "add" button
		this._txtField.keypress($.proxy(this._onKeyPress, this));
		this._txtField.keyup($.proxy(this._onKeyUp, this));
		this.element.find("input[type='button']").click($.proxy(this._onAddButtonClick, this));

		// Bind event handlers: hierarchy browsing and selection
		this._suggestionsContainer.click($.proxy(this._onSuggestionClick,this));

		// All ready, put the focus into the text field
		//this._txtField.focus();
	},

	_renderRoots: function() {
		this._renderConcepts(this._model.roots);
	},

	_renderNarrowerConcepts : function(tagURI){
		var tag = this._model.nodes[tagURI];

		var children = [];
		$.each(tag.narrowerURIs, $.proxy(function(i, URI){
			children.push(this._model.nodes[URI]);
		},this));
		this._renderConcepts(children);
	},

	_renderBroaderConcepts : function(tagURI) {
		// See if this tag has a parent
		var tag = this._model.nodes[tagURI];
		if (!tag || !tag.broaderURI)
			return;

		// If it does, look for all its siblings
		var parent = this._model.nodes[tag.broaderURI];

		var siblings = this._model.getSiblingsAndNode(parent.URI);
		this._renderConcepts(siblings);
	},

	_renderConcepts : function(nodes) {
		this._empty();

		$.each(nodes, $.proxy(function(i, root){
			this._suggestionsContainer.append(this._renderConcept(root));
		}, this));
	},

	_renderConcept : function(node) {
		var h = "<p class='tag' taguri='" + node.URI + "'>";

		if (node.broaderURI) {
			h += "<span class='up'></span>";
		}

		h += node.label;

		if (node.narrowerURIs.length > 0) {
			h += "<span class='down'></span>";
		}

		h += "</p>";

		return h;
	},

	_empty : function(){
		this._suggestionsContainer.empty();
	},


	///////////////////////////
	// Private Implementation - User Interface - Event Handlers
	///////////////////////////

	_onAddButtonClick : function(){
		this._log("Button clicked");

		if (this.options.buttonClick) {
			this.options.buttonClick(this._txtField.attr("value"));
		}

		// Note: The field should probably get cleared after the tag is
		// added successfully
		this._txtField.attr("value", "");
		this._txtField.focus();
	},

	_onKeyPress : function(e) {
		//console.log(e);
	},

	_onKeyUp : function(e) {
		//console.dir(e);
		if (e.keyCode == 13) {
			this._onAddButtonClick();
		} else {
			this._loadSuggestions();
		}
	},

	_onSuggestionClick : function(e) {
		var target = e.target;
		var tagURI = $(target).closest("p.tag").attr("taguri");
		
		if (target.tagName == "SPAN") {
			if (target.className == "up") {
				this._renderBroaderConcepts(tagURI);

			} else if (target.className == "down") {
				this._renderNarrowerConcepts(tagURI);
			}
		} else if (target.tagName=="P") {
			if (this.options.suggestionClicked) {
				var label = this._model.nodes[tagURI].label;
				var schemeURI = this._model.nodes[tagURI].schemeURI;
				this.options.suggestionClicked(tagURI, label, schemeURI);
			}
		}
	},

	_loadSuggestions : function() {
		var q = this._txtField.attr("value");

		if (q == "") {
			this._renderConcepts([]);
			
		} else {
			var prefix = null;
			var label = null;

			if (q.indexOf(":")) {
				prefix = q.substr(0, q.indexOf(":"));
				label = q.substr(q.indexOf(":") + 1);
			} else {
				label = q;
			}

			var matches = this._model.findConcepts(prefix, label);
			this._renderConcepts(matches);
		}


	},

	///////////////////////////
	// Private Implementation - Web Services
	///////////////////////////


	/**
	 * Loads controlled tags and "taxonomy prefixes". Once both loaded,
	 * populates the model.
	 *
	 */
	_loadTaxonomies : function(){

		var controlledTags = null;
		var prefixesSparqlResult = null;

		this._log("WS: Get controlled tags");
		
		jQuery.ajax({
			url: this.options.serverBaseURL + "social/tag/list/controlled",
			type: "GET",
			success: $.proxy(function(data, extStatus, xhr){
				this._log("WS: Received controlled tags");

				controlledTags = data;

				if (controlledTags && prefixesSparqlResult) {
					this._model.addPrefixes(prefixesSparqlResult);
					this._model.addNodes(controlledTags);
					this._renderRoots();
				}

			},this),
			error: $.proxy(function(xhr, textStatus, errorThrown) {
				this._log("WS: Failed to get controlled tags");
			},this)
		});

		this._log("WS: Load taxonomy prefixes via SPARQL query");

		var q = "SELECT ?s ?o WHERE { ?s <http://www.oracle.com/KiWi/hasPrefix> ?o. }";
		jQuery.ajax({
			url: this.options.serverBaseURL + "query/sparql"
					+ "?q=" + encodeURIComponent(q)
					+ "&wt=json",
			type: "GET",
			success: $.proxy(function(data, extStatus, xhr){
				this._log("WS: Received taxonomy prefixes");

				prefixesSparqlResult = data;

				if (controlledTags && prefixesSparqlResult) {
					this._model.addPrefixes(prefixesSparqlResult);
					this._model.addNodes(controlledTags);
					this._renderRoots();
				}
			},this),
			error: $.proxy(function(xhr, textStatus, errorThrown) {
				this._log("WS: Failed to load taxonomy prefixes");
			},this)
		});
	},

	////////////////////
	// Public API
	////////////////////

	focus : function(){
		this._txtField.focus();
	}

});

HierarchyModel = function(){

	/////////////////
	// Private members
	/////////////////

	
	/////////////////
	// Public API
	/////////////////

	// Return the public API
	return {
		nodes : {},
		roots : [],
		prefixes: {},

		addNodes : function(nodesTriples){

			// First, go through all nodes in the JSON RDF representation
			// (returned from the web service) and create a simplified
			// object model (_nodes) which is a map of
			// node URI and an object with properties:
			// URI, label, broaderURI, narrowerURIs, schemeURI

			// Also, while doing this, keep a list of top concepts

			$.each(nodesTriples, $.proxy(function(URI, props){
				var node = {
					URI: URI,
					narrowerURIs: []
				};

				// We assume only max one "broader" as this is a tree (taxonomy)
				if (props["http://www.w3.org/2004/02/skos/core#broader"]) {
					node.broaderURI = props["http://www.w3.org/2004/02/skos/core#broader"][0]["value"];
				} else {
					node.broaderURI = null;
				}

				if (props["http://www.w3.org/2004/02/skos/core#narrower"]) {
					var narrower = props["http://www.w3.org/2004/02/skos/core#narrower"];
					for (var i = 0; i < narrower.length; i++) {
						node.narrowerURIs.push(narrower[i]["value"]);
					}
				}

				node.label = props["http://www.w3.org/2004/02/skos/core#prefLabel"][0]["value"];
				node.schemeURI = props["http://www.w3.org/2004/02/skos/core#inScheme"][0]["value"];

				// If this is a top concept, add it to the roots list
				if (props["http://www.w3.org/2004/02/skos/core#topConceptOf"]) {
					this.roots.push(node);

					// Also link prefix object to the root node
					this.prefixes[this.findPrefixForConceptSchemeURI(node.schemeURI)].rootNode = node;
				}

				// Also add this node object to the list of node objects
				this.nodes[URI] = node;
			}, this));
		},
		
		
		/**
		 * Returns siblings of the concept specified by given URI and
		 * the concept itself. In other words, returns all concepts
		 * from the same level as the concept specified by the URI.
		 * 
		 */
		getSiblingsAndNode : function(tagURI) {
			var tag = this.nodes[tagURI];
			if (!tag)
				return [];

			// Go through all nodes; if a node has the same parent
			// as our tag, it's a sibling
			var siblings = [];
			$.each(this.nodes, $.proxy(function(uri, node){
				if (node.broaderURI == tag.broaderURI) {
					siblings.push(node);
				}
			}, this));

			return siblings;
		},

		addPrefixes : function(sparqlResult) {
			for (var i = 0; i < sparqlResult.results.bindings.length; i++) {
				var res = sparqlResult.results.bindings[i];

				var conceptSchemeURI = res["s"].value;
				var prefix = res["o"].value;

				this.prefixes[prefix] = conceptSchemeURI;
			}
		},

		findPrefixForConceptSchemeURI : function(conceptSchemeURI){
			for (var prefix in this.prefixes) {
				if (this.prefixes[prefix] == conceptSchemeURI)
					return prefix;
			}

			return null;
		},

		/**
		 * Finds all concepts (controlled tags) matching the query.
		 *
		 * If prefix is not specified, we search all taxonomies.
		 * If prefix is specified, the search is restricted to
		 * taxonomy identified by the label.
		 *
		 *
		 */
		findConcepts : function(prefix, label) {
			var conceptSchemeURI = null;
			if (prefix) {
				conceptSchemeURI = this.prefixes[prefix];
			}

			// We match case-insensitive
			label = label.toLowerCase();

			var matches = [];
			$.each(this.nodes, $.proxy(function(uri, node){
				// If we are matching on conceptSchemeURI and
				// this node does not match it, continue with next node
				if ((conceptSchemeURI) && (conceptSchemeURI != node.schemeURI)) {
					return true;
				}

				// If the node label does not start with the query label
				// continue with next node
				if (node.label.toLowerCase().indexOf(label) != 0) {
					return true;
				}

				// All passed, this is a match
				matches.push(node);

			}, this));

			// Sort the matches alphabetically
			matches = matches.sort(function(a,b){
				if (a.label < b.label)
					return -1
				else if (a.label > b.label)
					return 1
				else
					return 0;
			});
			return matches;
		}
	}
}